﻿using System;
using System.Collections.Generic;
using System.Data;
using System.Data.Sql;
using System.Data.SqlClient;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Linq;
using System.Runtime.Serialization.Formatters.Binary;
using System.Runtime.Serialization;
using System.Text;
using System.Web;
using System.Web.SessionState;

using Microsoft.SqlServer.Types;

namespace MapViewer
{
    public class Drawing
    {
        //---------------------------------------------------------------------
        // Input Properties - values used to generate the image and the 
        // rollover / clickable area HTML controls
        //---------------------------------------------------------------------
        
        // Do we generate a clickable map or not?
        public bool GenerateClickMap
        { get; set; }
        
        // URL to go to when a region on the map is clicked
        public string MapUrl
        { get; set; }

        // value to pass as 'key', added to the MapUrl link
        public String MapKey
        { get; set; }

        // value to pass as 'field', added to the MapUrl link
        public String MapField
        { get; set; }

        // unit displayed in scale text in corner of control
        public String MapUnits
        { get; set; }

        // height and width of image
        public float width
        { get; set; }

        public float height
        { get; set; }

        //---------------------------------------------------------------------
        // Output Properties - diagnostics, the image GUID and the rollover
        // area HTML controls
        //---------------------------------------------------------------------
        
        // diagnostics 
        public String QueryDurations
        { get; set; }

        public int geocount
        { get; set; }

        public int pathcount
        { get; set; }

        // output image name
        public Guid imagename
        { get; set; }

        // output area map text
        private StringBuilder _areamap = new StringBuilder("");
        public String areamap
        {
            get { return _areamap.ToString(); }
            set { _areamap.Remove(0, _areamap.Length); _areamap.Append(value); } 
        }

        // scale of image
        public float scale
        { get; set; }

        //---------------------------------------------------------------------
        // Internal Variables
        //---------------------------------------------------------------------

        // X and Y offset needed to get the upper right corner to 0,0
        private PointF offsets;

        // data returned by layer query, will hold the shapes we draw
        internal DataTable data
        { get; set; }

        //---------------------------------------------------------------------
        // Initialize output properties
        //---------------------------------------------------------------------
        internal void Initialize()
        {
            scale = 0;
            geocount = 0;
            pathcount = 0;
        }
        
        //---------------------------------------------------------------------
        // Main drawing control - cycle over the layers, get data and call the drawing routines
        //---------------------------------------------------------------------
        internal void DrawAllLayers(LayerManager layermanager)
        {
            // do some initial setup
            Initialize();
            
            // reset diagnostic
            QueryDurations = "";

            // create bitmap and graphics context
            Bitmap bmp = new Bitmap((int)width, (int)height);
            Graphics graph = Graphics.FromImage(bmp);

            // initial setup
            graph.FillRectangle(Brushes.White, 0, 0, (int)width, (int)height);
            graph.SmoothingMode = SmoothingMode.AntiAlias;

            // Check for no matching data, or a null region
            if (layermanager.Envelope.IsNull)
            {
                DisplayNoDataMessage(graph);
            }
            else
            {
                // get offsets to zero-bias our coordinates
                offsets = new PointF(
                    (float)layermanager.Envelope.STStartPoint().STX,
                    (float)layermanager.Envelope.STStartPoint().STY);

                // set the ration between the "size" of the shapes returned and the requested size of the image
                // of the final graphic
                scale = layermanager.GetOutputScale();

                // process each layer registerd in layermanager
                foreach (LayerInfo layer in layermanager)
                {
                    // get data for each layer that lies within our envelope
                    data = layer.getByRegion();

                    // diagnostic 
                    QueryDurations += layer.name + " - " + layer.duration.ToString() + "&nbsp;&nbsp;&nbsp;";

                    if (0 != data.Rows.Count)
                    {
                        // draw the selected shapes to the output bitmap
                        DrawGeometry(layer, data, graph);

                        // if we are showing key values for this layer, display the text
                        if (layer.displaykey)
                            DrawTextValues(layer, data, graph);

                        // if we are generating a "click map" in html for the selected layer, create it
                        if (GenerateClickMap && (layer.name == layermanager.SelectionLayerName))
                        {
                            // create the HTML 'map' for clickable areas in the image
                            _areamap.Append(CreateArea(layer, data, scale));

                            // create the data structures for rollover display
                            //if (0 != layer.DisplayColumns.Count)
                            //    json.Append(CreateJSON(data, layer, (scale) * (scale)));
                        }
                    }
                }

                // set the area map HTML to output variable
                areamap = _areamap.ToString();
            }

            // Lastly, we need to flip the image because the shape space goes "up" from
            // the origin, while the drawing space goes "down" from the origin.
            bmp.RotateFlip(RotateFlipType.RotateNoneFlipY);

            // save the image
            System.IO.MemoryStream ms = new System.IO.MemoryStream();
            bmp.Save(ms, System.Drawing.Imaging.ImageFormat.Png);

            // write the image to the cache so it can be requested by the browser
            imagename = Guid.NewGuid();
            HttpContext curr = HttpContext.Current;
            curr.Cache.Insert(imagename.ToString(), ms.GetBuffer(), null, DateTime.Now.AddSeconds(120), TimeSpan.Zero);

            // clean up our graphics objects
            graph.Dispose();
            bmp.Dispose();
            ms.Dispose();
        }

        //---------------------------------------------------------------------
        // Draw all the shapes in a layer. Keep out selected items and draw
        // them last so they appear "on top" of the other shapes
        //---------------------------------------------------------------------
        void DrawGeometry(LayerInfo layer, DataTable data, Graphics graph)
        {
            // holder for selected items. These are printed out of order - they are 
            // printed last so they'll be 'on top' of other shapes
            List<GraphicsPath> selecteditems = new List<GraphicsPath>();

            // process the data - get the geo column, turn it into graphics paths and
            // draw them to our output image
            foreach (DataRow row in data.Rows)
            {
                // diagnostic
                geocount++;

                // get the geo data from the database
                SqlGeometry Geom = (SqlGeometry)row[layer.geoColumn];

                // convert geometry to a list of graphics paths
                List<GraphicsPath> list = Geom.ConvertToPaths(offsets, scale);
                foreach (GraphicsPath path in list)
                {
                    // diagnostics
                    pathcount++;

                    // if this row of data is a selected item, keep it aside and 
                    // draw it last
                    if (layer.selecteditems.Contains(row[layer.keycolumn].ToString()))
                    {
                        selecteditems.Add(path);
                    }
                    else
                    {
                        // 'normal' drawing - just draw the lines in the path
                        graph.DrawPath(layer.LayerDrawingOptions.LinePen, path);

                        // if we have a closed region, then fill it in. Get brushes that may
                        // be key-specific if a legend has been specified
                        if ((path.PathTypes[path.PointCount - 1] & 128) == 128)
                            graph.FillPath(layer.LayerDrawingOptions.GetBrush(row[layer.keycolumn].ToString()), path);
                    }
                }
            }

            // now draw selected items. This is so they'll print on 'top' of their neighbors
            foreach (GraphicsPath path in selecteditems)
            {
                // selected item drawing - just draw the lines in the path
                graph.DrawPath(layer.LayerDrawingOptions.SelectedLinePen, path);

                // if we have a closed region, then fill it in
                if ((path.PathTypes[path.PointCount - 1] & 128) == 128)
                    graph.FillPath(layer.LayerDrawingOptions.SelectedBodyBrush, path);
            }
        }

        //---------------------------------------------------------------------
        // Draw text values for all shapes retrieved.
        //---------------------------------------------------------------------
        void DrawTextValues(LayerInfo layer, DataTable data, Graphics graph)
        {
            // if the scaling factor is bigger than the text clip value, don't draw text
            if (layer.LayerDrawingOptions.TextClip < (1 / scale))
                return;

            // hold accumulated geometries if we're condensing text down to once per
            // all geometries with the same key
            SqlGeometry accumulator = (SqlGeometry)data.Rows[0][layer.geoColumn];

            // hold current key value for condensing text to once 
            String currentvalue = data.Rows[0][layer.keycolumn].ToString();

            // draw the identifying value for each shape
            for (int index = 0; index < data.Rows.Count; index++)
            {
                DataRow row = data.Rows[index];
                //String idvalue = row[layer.keycolumn].ToString();
                SqlGeometry Geom = (SqlGeometry)row[layer.geoColumn];
                String idvalue = String.Empty;
                foreach (LayerInfo.DisplayColumnDetails item in layer.DisplayColumns)
                    idvalue += row[item.ColumnName].ToString() + " ";


                if (layer.condensekey)
                {
                    // if the current key value changes then print and reset holders
                    if (idvalue != currentvalue)
                    {
                        DrawIndividualValue(layer, currentvalue, accumulator, graph);
                        accumulator = Geom;
                        currentvalue = idvalue;
                    }
                    // if we're at the end of our data then print
                    else if (data.Rows.Count == index + 1)
                    {
                        // add in last geometry and print
                        accumulator = accumulator.STUnion(Geom);
                        DrawIndividualValue(layer, currentvalue, accumulator, graph);
                    }
                    // otherwise just accumulate geometries for same-key valued rows
                    else
                    {
                        accumulator = accumulator.STUnion(Geom);
                    }
                }
                // otherwise just print every value
                else
                {
                    DrawIndividualValue(layer, idvalue, Geom, graph);
                }
            }

        }

        //---------------------------------------------------------------------
        // Draw the shape's text marker (keycolumn)
        //---------------------------------------------------------------------
        void DrawIndividualValue(LayerInfo layer, String idvalue, SqlGeometry Geom, Graphics graph)
        {
            //-------------------------------------------------------------
            // Do some initial checks before we try to draw any text
            //-------------------------------------------------------------

            // ... of course, if there isn't a value, don't try to draw it.
            if (String.IsNullOrEmpty(idvalue))
                return;

            // measure the string in base graph
            SizeF size = graph.MeasureString(idvalue,
                layer.LayerDrawingOptions.DisplayFont);

            // If the text will be longer than the object it's for, don't draw it
            if (size.Width > ((float)Geom.STLength() * scale))
                return;

            // reorder the points in the geometry
            Geom = Geom.MakeValid().STUnion(Geom.STStartPoint());

            //-------------------------------------------------------------
            // examine the geometry object and determine where we want to
            // draw the text in relation to the underlying area or line
            //-------------------------------------------------------------

            float angle = 0;
            PointF linestart = new PointF(0, 0);
            PointF centroid = new PointF(0, 0);

            // Get the line segment we're going to put the text next to, or area
            // that we'll center the text in
            PointF[] points = Geom.GetPrintLocation();
            if (2 == points.Length)
            {
                // two points returned means we're drawing text near a line
                // and at the same angle.

                // get delta x and y to calculate the midpoint as well as the angle
                float dx = (float)(points[0].X - points[1].X);
                float dy = (float)(points[0].Y - points[1].Y);

                // get the midpoint of the line, we'll use the midpoint to help
                // center the text next to the line
                centroid.X = points[0].X - (0.5f * dx);
                centroid.Y = points[0].Y - (0.5f * dy);

                // get the angle of the line so we can angle the text to follow the line
                if (0 != dx)
                    angle = -(180 / (float)Math.PI) * (float)(Math.Atan(dy / dx));
            }
            else
            {
                // if we have a polygon, we already have it's midpoint and we're
                // not doing anything with the angle
                centroid = points[0];
            }

            //-------------------------------------------------------------
            // Do manual scaling. We have to do this so that we don't run
            // into memory errors with large bitmaps for the text images
            //-------------------------------------------------------------

            // zero base our coordinates for drawing
            float x = centroid.X - offsets.X;
            float y = centroid.Y - offsets.Y;

            // adjust for scale so we're printing in the bitmap
            x = x * scale;
            y = y * scale;

            // adjust for the size of the image, since drawing is from the corner, not the center
            x -= size.Width / 2;
            y -= size.Width / 2;

            //-------------------------------------------------------------
            // Create and draw our text into a temporary bitmap
            //-------------------------------------------------------------

            // Create bitmap for our string
            Bitmap textmap = new Bitmap((int)size.Width, (int)size.Width);
            Graphics textgraph = Graphics.FromImage(textmap);
            textgraph.TextRenderingHint = System.Drawing.Text.TextRenderingHint.AntiAlias;

            // diagnostic only
            //textgraph.DrawRectangle(new Pen(Brushes.Black, 1), 0, 0, size.Width-1, size.Width-1);

            // create matrix for rotating text and assign to graph
            Matrix textmatrix = new Matrix();
            textmatrix.RotateAt(angle, new PointF(textmap.Width / 2, textmap.Height / 2));
            textgraph.Transform = textmatrix;

            // draw the text into our temp bitmap
            textgraph.DrawString(idvalue, layer.LayerDrawingOptions.DisplayFont, Brushes.Black,
                new PointF(textmap.Width / 2 - size.Width / 2, textmap.Height / 2 - size.Height / 2));

            // flip bitmap so the text will be rightside up (after re-flipping the main image)
            textmap.RotateFlip(RotateFlipType.RotateNoneFlipY);

            // finally write our text to the main image using our calculated x and y coords
            graph.DrawImage(textmap, x, y);

            // ... and cleanup
            textmatrix.Dispose();
            textgraph.Dispose();
            textmap.Dispose();
        }

        //---------------------------------------------------------------------
        // Build HTML 'Map' tag with an area for each external and internal polygon
        //---------------------------------------------------------------------
        String CreateArea(LayerInfo layer, DataTable data, float scale)
        {
            // Create stringbuilder to hold our html output
            StringBuilder tag = new StringBuilder();

            // draw each complex shape
            //foreach (GeomInfo shapeInfo in shapes.lines.Values)
            foreach (DataRow row in data.Rows)
            {
                SqlGeometry Geom = (SqlGeometry)row[layer.geoColumn];
                String idvalue = row[layer.keycolumn].ToString();
                String idfield = layer.keycolumn;

                // don't draw an image map area for ultra small areas
                float areascale = (float)(Geom.STArea()) * (scale) * (scale);
                if (35 >= areascale)
                    continue;

                // print the clickable region and rollover html for each shape
                for (int geomIndex = 0; geomIndex < (Int32)Geom.STNumGeometries(); geomIndex++)
                {
                    SqlGeometry subGeom = Geom.STGeometryN(geomIndex + 1);

                    // only deal with polygons - i.e. you can't rollover a line
                    if (subGeom.STGeometryType() == "Polygon")
                    {
                        // start the tab
                        tag.Append("<area shape=\"polygon\" target='_top' ");

                        // only exterior ring gets href and alt values
                        char connector = '?';
                        if (MapUrl.ToString().Contains('?'))
                            connector = '&';

                        // add in layer reference to target url and reset connector
                        tag.Append("href=\"").Append(MapUrl).Append(connector).Append("layer=").Append(layer.name);
                        connector = '&';

                        // add key field name to target url
                        tag.Append(connector).Append(MapField).Append("=").Append(idfield);

                        // now add key reference to target url
                        tag.Append(connector).Append(MapKey).Append("=").Append(idvalue).Append("\" ");

                        // add alt text
                        tag.Append("alt=\"").Append(idvalue).Append("\" ");

                        // if we have a rollover display for this layer, add the mouseover tags
                        if ((0 != layer.DisplayColumns.Count) && (35 < (Convert.ToDouble(row["geo_Area"]) * scale * scale)))
                        {
                            // create the data structures inline since a separate object doesn't 
                            // work with AJAX calls.
                            tag.Append(BuildShowTipsCommand(row, layer));
                        }

                        // handle points from exterior ring
                        tag.Append("coords=\"").Append(subGeom.STExteriorRing().PointsfromRing(height, offsets.X, offsets.Y, scale)).Append("\" ");

                        // close area tag
                        tag.Append("/>");

                        // handle points from each interior ring
                        for (int lineIndex = 0; lineIndex < subGeom.STNumInteriorRing(); lineIndex++)
                        {
                            tag.Append("<area shape=\"polygon\" ");
                            // handle points from exterior ring
                            tag.Append("coords=\"").Append(subGeom.STExteriorRing().PointsfromRing(height, offsets.X, offsets.Y, scale)).Append("\" ");
                            // close area tag
                            tag.Append("/>");
                        }
                    }
                }
            }

            // return our html map 
            return tag.ToString();
        }

        //---------------------------------------------------------------------
        // Create JSON-style data structures for rollover data display
        //---------------------------------------------------------------------
        protected String BuildShowTipsCommand(DataRow row, LayerInfo layer)
        {
            StringBuilder tip = new StringBuilder();
            tip.Append("onmouseover=\"javascript:").Append("ShowTip2([");

            // write a displayname / displayvalue array entry for each display column
            foreach (LayerInfo.DisplayColumnDetails item in layer.DisplayColumns)
            {
                tip.Append("{\'name\':\'")
                    .Append(item.DisplayName)
                    .Append("\', \'val\':\'")
                    .Append(row[item.ColumnName])
                    .Append("\'},");
            }

            // remove trailing comma
            tip.Remove(tip.Length - 1, 1);
            // close out the call
            tip.Append("]);\" ");
            tip.Append("onmouseout=\"HideTip();\" ");

            return tip.ToString();
        }

        //---------------------------------------------------------------------
        // Create JSON-style data structures for rollover data display
        // 
        // Note: Saved for reference. Had to move to "inline" objects in the 
        // area tag rather than referencing a separate JSON object because
        // when the JSON script was downloaded via the AJAX call, it would not
        // be executed. I.e. it just didn't exist as far as the browser was 
        // concerned.
        //---------------------------------------------------------------------
        /*
        protected String CreateJSON(DataTable table, LayerInfo layer, float areaajustment)
        {
            StringBuilder tag = new StringBuilder();

            // start layer - note trailing space at the end so we don't have invalid
            // script in our web page.
            tag.Append("var layer_").Append(layer.name).Append(" = { ");

            foreach (DataRow row in table.Rows)
            {
                // skip keys for areas too small to have a map entry
                if (35 >= (Convert.ToDouble(row["geo_Area"]) * areaajustment))
                    continue;

                // start key value
                tag.Append("\"key_").Append(row[layer.keycolumn]).Append("\":[");

                // write data display array entries
                foreach (LayerInfo.DisplayColumnDetails item in layer.DisplayColumns)
                {
                    tag.Append("{\"name\":\"").Append(item.DisplayName).Append("\", \"val\":\"").Append(row[item.ColumnName]).Append("\"},");
                }

                // remove trailing comma
                tag.Remove(tag.Length-1, 1);

                // close key value and append a newline
                tag.Append("]\n,");
            }

            // remove trailing quote and replace with crlf
            tag.Remove(tag.Length - 1, 1).Append("\n");
             
            // close layer
            tag.Append("}").Append("\n");
            return tag.ToString();
        }
        */

        //---------------------------------------------------------------------
        // Display a 'no data found' message
        //---------------------------------------------------------------------
        protected void DisplayNoDataMessage(Graphics graph)
        {
            // no data message
            String nodatamessage = "No data found for this search or region.\r\nPlease try again.";

            // display font and size
            Font displayfont = new Font("Arial", 10);
            SizeF size = graph.MeasureString(nodatamessage, displayfont);

            // draw the string to our own bitmap so we can flip and rotate it easily
            Bitmap b = new Bitmap((int)(size.Width + 1), (int)(size.Width + 1));
            Graphics bg = Graphics.FromImage(b);
            bg.TextRenderingHint = System.Drawing.Text.TextRenderingHint.AntiAlias;

            // draw the string, centered in the temp image
            bg.DrawString(nodatamessage, displayfont,
                Brushes.Black, new PointF(0, b.Height / 2 - size.Height / 2));
            bg.Dispose();

            // flip it so it'll be right-side-up when we re-flip it later
            b.RotateFlip(RotateFlipType.RotateNoneFlipY);

            // get the location in the base image to write the image with our string
            PointF linestart = new PointF(width / 2, height / 2);
            linestart.X = linestart.X - (b.Width / 2);
            linestart.Y = linestart.Y - (b.Height / 2);
            graph.DrawImage(b, linestart);

            // cleanup
            b.Dispose();

        }

    }
}
